//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
using System.ServiceModel;
using System.Xml.Serialization;
using SR = Microsoft.Research.DataLayer;

namespace DataLayerBroker
{
    public static class Constants
    {
        public const string DataLayerNamespace = "Microsoft.Research.DataLayer";
    }

    [ServiceContract]
    public interface IDataLayerBroker
    {
        //
        // Core Registry services
        //
        [OperationContract(Name = "SerializationProtocol")]
        void SetSerializationProtocol(SerializationProtocol protocol);

        [OperationContract(Name = "ObjectParam")]
        void Object(Storage obj, SR.Connection.Action action, ref List<Parameter> paramList);

        [OperationContract(Name = "ObjectParamResults")]
        void Object(Storage obj, SR.Connection.Action action, ref List<Parameter> paramList, out List<XferObjectInstance> results);

        [OperationContract(Name = "RelationSimple")]
        void Relation(SR.Connection.Action action, XferObjectReference obj1, XferObjectReference obj2);

        [OperationContract(Name = "RelationSimpleExtraData")]
        void Relation(SR.Connection.Action action, XferObjectReference obj1, XferExtraRelDataReference obj2);

        [OperationContract(Name = "RelationParamResults")]
        void Relation(SR.Connection.Action action, XferObjectReference obj1, string field, Storage type2, out List<XferObjectInstance> results);

        [OperationContract(Name = "RelationParamRev")]
        void Relation(SR.Connection.Action action, XferObjectReference obj1, Storage obj2, string revKey, ref List<Parameter> paramList);

        //
        // Data atomicity control
        //

        [OperationContract]
        void ConnAtom_BeginEdit();

        [OperationContract]
        void ConnAtom_EndEdit();

        [OperationContract]
        void ConnAtom_CancelEdit();

        [OperationContract]
        void Connect();
    }

    [DataContract]
    public class Storage
    {
        [DataMember]
        protected byte[] storage;

        public SerializationProtocol protocol;

        public SR.Connection.Storage FromNetwork()
        {
            SR.Connection.Storage ret;
            MemoryStream ms = new MemoryStream(this.storage);

            if (SerializationProtocol.XML == protocol)
            {
                XmlSerializer fmt = new XmlSerializer(typeof(SR.SRStorage));
                ret = (SR.Connection.Storage)fmt.Deserialize(ms);
            }
            else
            {
                IFormatter fmt = new SoapFormatter();
                ret = (SR.Connection.Storage)fmt.Deserialize(ms);
            }
            return ret;
        }
    }

    [DataContract]
    public class Parameter
    {
        [DataMember]
        protected byte[] parameter;

        public SerializationProtocol protocol;

        public SR.Parameter FromNetwork()
        {
            SR.Parameter ret;
            MemoryStream ms = new MemoryStream(this.parameter);
            if (protocol == SerializationProtocol.XML)
            {
                XmlSerializer fmt = new XmlSerializer(typeof(SR.Parameter));
                ret = (SR.Parameter)fmt.Deserialize(ms);
            }
            else
            {
                IFormatter fmt = new SoapFormatter();
                ret = (SR.Parameter)fmt.Deserialize(ms);
            }
            return ret;
        }

        private SR.IObject CreateObject(SR.Connection conn, Assembly asm, SR.XferObjectReference2 refObj)
        {
            SR.Connection.Storage storage = refObj.UnPackObjType();
            System.Type[] typeList = asm.GetTypes();
            System.Type typeToLoad = null;

            // Attempt #1: Try to find via FullName
            string typeFullName = string.Format("{0}.{1}", Constants.DataLayerNamespace, storage.Name);
            foreach (System.Type typeObj in typeList)
            {
                if (typeObj.FullName.Equals(typeFullName, StringComparison.InvariantCultureIgnoreCase))
                {
                    // We found the type
                    typeToLoad = typeObj;
                    break;
                }
            }

            if (typeToLoad == null)
            {
                throw new NotImplementedException(string.Format("Type '{0}' is not supported or not found", storage.Name));
            }

            // Load the object, by ID, using Load(Guid, Connection) method
            MethodInfo[] all = typeToLoad.GetMethods();
            MethodInfo loadMethod = typeToLoad.GetMethod("Load", BindingFlags.Static | BindingFlags.Public,
                System.Type.DefaultBinder, new System.Type[] { typeof(Guid), typeof(SR.Connection) }, null);

            if (loadMethod == null)
            {
                throw new NotImplementedException(string.Format("Type '{0}' does not have Load(Guid, Connection) method", storage.Name));
            }

            SR.IObject loadedObject = (SR.IObject)loadMethod.Invoke(null, new object[] { refObj.Id, conn });

            return loadedObject;
        }

        private void RestoreField(SR.Connection conn, ref SR.SearchClauseWrapper clauseWrapper)
        {
            if (clauseWrapper.Field is SR.XferEnumReference)
            {
                SR.XferEnumReference reference = clauseWrapper.Field as SR.XferEnumReference;
                Assembly asm = Assembly.GetAssembly(typeof(SR.User));
                Assembly asm2 = Assembly.GetAssembly(typeof(SR.EnumFieldBase));
                System.Type classType = asm.GetType(reference.objectType);
                MethodInfo method = classType.GetMethod(reference.staticMethodName);

                object cond = Enum.Parse(asm2.GetType(reference.enumConditionType, false, true), reference.enumConditionValue);
                object condValue = Enum.Parse(asm.GetType(reference.enumValueType, false, true), reference.enumValueValue);
                SR.SearchClauseWrapper temp = (SR.SearchClauseWrapper)method.Invoke(null, new object[] { cond, condValue });
                clauseWrapper.Field = temp.Field;
            }
            else if (clauseWrapper.Field is SR.XferObjectField || clauseWrapper.Field is SR.RelationFieldBase
                || clauseWrapper.Field is SR.RelationObjectFieldBase)
            {
                SR.XferObjectField objRef = clauseWrapper.Field as SR.XferObjectField;
                Assembly asm = Assembly.GetAssembly(typeof(SR.User));
                Assembly asm2 = Assembly.GetAssembly(typeof(SR.EnumFieldBase));
                System.Type classType = asm.GetType(objRef.objectType);
                MethodInfo method = classType.GetMethod(objRef.staticMethodName);

                object cond = Enum.Parse(asm2.GetType(objRef.enumConditionType, false, true), objRef.enumConditionValue);
                SR.IObject searchObj = CreateObject(conn, asm, (SR.XferObjectReference2)objRef.searchObject);
                SR.SearchClauseWrapper temp = (SR.SearchClauseWrapper)method.Invoke(null, new object[] { cond, searchObj });
                clauseWrapper.Field = temp.Field;
                clauseWrapper.ClauseImpl.Value = searchObj;
            }
        }

        private void RestoreSearchFields(SR.Connection conn, ref SR.ISearchClause clause)
        {
            if (clause is SR.SearchGroupBinary)
            {
                SR.ISearchGroupBinary binaryClause = clause as SR.ISearchGroupBinary;
                SR.ISearchClause clause1 = binaryClause.P1;
                SR.ISearchClause clause2 = binaryClause.P2;
                RestoreSearchFields(conn, ref clause1);
                RestoreSearchFields(conn, ref clause2);
            }
            else if (clause is SR.SearchGroupUnary)
            {
                SR.ISearchGroupUnary unaryClause = clause as SR.ISearchGroupUnary;
                SR.ISearchClause clause1 = unaryClause.P1;
                RestoreSearchFields(conn, ref clause1);
            }
            else
            {
                SR.SearchClauseWrapper clauseWrapper = clause as SR.SearchClauseWrapper;
                RestoreField(conn, ref clauseWrapper);
            }
        }

        public SR.Parameter FromNetworkSearch(SR.Connection conn)
        {
            SR.Parameter ret = null;
            MemoryStream ms = new MemoryStream(this.parameter);
            if (SerializationProtocol.XML == this.protocol)
            {
                XmlSerializer fmt = new XmlSerializer(typeof(SR.Parameter));
                ret = (SR.Parameter)fmt.Deserialize(ms);
            }
            else
            {
                IFormatter fmt = new SoapFormatter();
                ret = (SR.Parameter)fmt.Deserialize(ms);
                SR.ISearchClause clause = ((SR.SearchQuery)ret.Value).GetQuery();
                RestoreSearchFields(conn, ref clause);
            }

            return ret;
        }

        public static Parameter ToNetwork(SR.Parameter src, SerializationProtocol protocol)
        {
            Parameter ret = new Parameter();
            MemoryStream ms = new MemoryStream();
            if (SerializationProtocol.XML == protocol)
            {
                XmlSerializer fmt = new XmlSerializer(typeof(SR.Parameter));
                fmt.Serialize(ms, src);
            }
            else
            {
                IFormatter fmt = new SoapFormatter();
                fmt.Serialize(ms, src);
            }
            ret.parameter = ms.ToArray();
            return ret;
        }

        public override string ToString()
        {
            SR.Parameter p = FromNetwork();
            return string.Format("Parameter: {0} [{1}] = '{2}'", p.Name, p.Type, p.Value);
        }
    }

    [DataContract]
    public class XferObjectReference
    {
        [DataMember]
        public Guid ID;

        [DataMember]
        public Storage Type;

        public SerializationProtocol protocol;

        [OperationContract]
        public SR.IObject Create(SR.Connection conn)
        {
            Type.protocol = this.protocol;
            SR.Connection.Storage type = Type.FromNetwork();

            // Use Reflection to load item
            Assembly asm = Assembly.GetAssembly(typeof(SR.User));
            System.Type[] typeList = asm.GetTypes();
            System.Type typeToLoad = null;

            // Attempt #1: Try to find via FullName
            string typeFullName = string.Format("{0}.{1}", Constants.DataLayerNamespace, type.Name);
            foreach (System.Type typeObj in typeList)
            {
                if (typeObj.FullName.Equals(typeFullName, StringComparison.InvariantCultureIgnoreCase))
                {
                    // We found the type
                    typeToLoad = typeObj;
                    break;
                }
            }

            if (typeToLoad == null)
            {
                throw new NotImplementedException(string.Format("Type '{0}' is not supported or not found", type.Name));
            }

            // Load the object, by ID, using Load(Guid, Connection) method
            MethodInfo[] all = typeToLoad.GetMethods();
            MethodInfo loadMethod = typeToLoad.GetMethod("Load", BindingFlags.Static | BindingFlags.Public,
                System.Type.DefaultBinder, new System.Type[] { typeof(Guid), typeof(SR.Connection) }, null);

            if (loadMethod == null)
            {
                throw new NotImplementedException(string.Format("Type '{0}' does not have Load(Guid, Connection) method", type.Name));
            }

            if (ID.Equals(Guid.Empty))
            {
                throw new SR.RecordNotFoundException("Data not found");
            }

            try
            {
                SR.IObject loadedObject = (SR.IObject)loadMethod.Invoke(null, new object[] { ID, conn });

                return loadedObject;
            }
            catch (TargetInvocationException e)
            {
                Exception err = e.InnerException;

                if (err is SR.RecordNotFoundException)
                    throw e.InnerException;
                else
                {
                    throw new InvalidOperationException("Problem with the object model", e);
                }
            }
        }
    }

    [DataContract]
    public class XferObjectInstance : SR.IObject
    {
        [DataMember]
        public Guid ID;

        [DataMember]
        public Storage Type;

        [DataMember]
        public List<Parameter> Fields;

        #region IObject Members

        Guid Microsoft.Research.DataLayer.IObject.ID
        {
            get { return ID; }
        }

        public Microsoft.Research.DataLayer.Connection.Storage ObjType
        {
            get { return Type.FromNetwork(); }
        }

        #region Methods not implemented. This object is purely for transfer across the wire
        public Microsoft.Research.DataLayer.IObject CopyTo(Microsoft.Research.DataLayer.Connection targetConnection)
        {
            throw new NotImplementedException();
        }

        public void Refresh()
        {
            throw new NotImplementedException();
        }

        public void Save()
        {
            throw new NotImplementedException();
        }

        public void Delete()
        {
            throw new NotImplementedException();
        }
        #endregion

        #endregion

        public override string ToString()
        {
            return string.Format("XferObjectInstance: {0} ({1})", ID, Type);
        }
    }


    [DataContract]
    public class XferExtraRelDataReference : XferObjectReference
    {
        [DataMember]
        public Guid ParentId;

        [DataMember]
        public Guid ChildId;

        [DataMember]
        public List<Parameter> ExtraData;

        public new SR.IObject Create(SR.Connection conn)
        {
            Type.protocol = this.protocol;
            SR.Connection.Storage type = Type.FromNetwork();

            // Use Reflection to load item
            Assembly asm = Assembly.GetAssembly(typeof(SR.User));
            System.Type[] typeList = asm.GetTypes();
            System.Type typeToLoad = null;

            // Attempt #1: Try to find via FullName
            string typeFullName = string.Format("{0}.{1}", Constants.DataLayerNamespace, type.Name);
            foreach (System.Type typeObj in typeList)
            {
                if (typeObj.FullName.Equals(typeFullName, StringComparison.InvariantCultureIgnoreCase))
                {
                    // We found the type
                    typeToLoad = typeObj;
                    break;
                }
            }

            if (typeToLoad == null)
            {
                throw new NotImplementedException(string.Format("Type '{0}' is not supported or not found", type.Name));
            }

            // Load the object, by ID, using Load(Guid, Connection) method
            ConstructorInfo defaultCons = typeToLoad.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null,
                        new System.Type[] { typeof(Guid), typeof(Guid), typeof(SR.Connection) }, null);

            List<SR.Parameter> extraData = new List<SR.Parameter>();
            for (int i = 0; i < this.ExtraData.Count; ++i)
            {
                extraData.Add(this.ExtraData[0].FromNetwork());
            }

            if (defaultCons == null)
            {
                throw new NotImplementedException(string.Format("Type '{0}' does not have default constructor", type.Name));
            }

            SR.IExtraRelationData loadedObject = (SR.IExtraRelationData)defaultCons.Invoke(new object[] { this.ParentId, this.ChildId, conn });

            foreach (SR.Parameter p in extraData)
            {
                FieldInfo fldInfo = typeToLoad.GetField(p.Name.ToLower(), BindingFlags.Instance | BindingFlags.NonPublic);
                if (null == fldInfo)
                {
                    throw new ArgumentException(string.Format("Parmeter '{0}' does not exist for {1}", p.Name, type.Name));
                }
                fldInfo.SetValue(loadedObject, GetFieldValue(fldInfo.FieldType, p.Value));

            }
            return (SR.IObject)loadedObject;
        }

        private object GetFieldValue(Type type, object value)
        {
            object converted = null;
            if (value == null)
                return null;

            if (typeof(Enum) == type.BaseType)
            {
                // Enum
                converted = Enum.Parse(type, value as string);
            }
            else if (typeof(DateTime) == type.BaseType)
            {
                // Datetime
                converted = DateTime.Parse(value as string);
            }
            else if (typeof(int) == type.BaseType)
            {
                // int
                converted = Convert.ToInt32(value);
            }
            else if (typeof(string) == type.BaseType)
            {
                // string
                converted = value as string;
            }
            else
            {
                throw new NotSupportedException(string.Format("Unsupported type '{0}'", value.GetType()));
            }

            return converted;
        }
    }

    /// <summary>
    /// Represents the supported Serializations
    /// </summary>
    public enum SerializationProtocol
    {
        /// <summary>
        /// Represents SOAP Serializer protocol
        /// It is a default Protocol
        /// </summary>
        SOAP,

        /// <summary>
        /// Represents XML Serializer protocol
        /// </summary>
        XML
    }
}
